(承上篇)
如同其他 web 框架的 Model 類別,通過建立 schema 模組我們可以做到對資料庫資料的存取。
今天我們可能需要一個對於文章資料表做存取的 schema,透過以下的指令,我們可以建立 post schema。
$ mix phx.gen.schema Post posts title:string content:string price:integer
* creating lib/sample_project/post.ex
* creating priv/repo/migrations/20201008132804_create_posts.exs
然後,就會創建一個 SampleProject.Post 這個模組。
lib/sample_project/post.ex
defmodule SampleProject.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :content, :string
field :price, :integer
field :title, :string
timestamps()
end
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :content, :price])
|> validate_required([:title, :content, :price])
end
end
然後,會建立一個遷移檔案,這個檔案紀錄了資料庫的架構變更,在專案遷移時,可以直接完成資料庫及相關資料表的建立,在這個檔案中,定義資料表相關資料,就可以進行新增資料表及欄位新增:
defmodule SampleProject.Repo.Migrations.CreatePosts do
use Ecto.Migration
def change do
create table(:posts) do
add :title, :string
add :content, :string
add :price, :integer
timestamps()
end
end
end
然後,輸入以下指令就可以進行遷移。
$ mix ecto.migrate
變更集 (ChangeSet)
在 Post schema 中,可以看到 changeset 這個函式,他的功能是在資料準備存入資料庫前,所需要進行的資料轉換或是驗證,因此變更集可以允許我們用更彈性的方式來處理資料,並過濾掉錯誤的資料。
lib/sample_project/post.ex
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :content, :price])
|> validate_required([:title, :content])
end
Ecto.Changeset.cast/3
的功能是用來進行資料的轉換及欄位的過濾,它接受三個參數,第一個參數是 %Post{} ,是以 Post Module 本身所定義的 struct 本身,而第二個參數是一個 map 代表資料本身要進行的轉換,第三個參數是允許通過的欄位所構成的 list。
以下面幾個範例來講:
因為content跟price都允許通過,寫map為空,所以資料不會有所改變。
iex> Ecto.Changeset.cast(%SampleProject.Post{content: "test", price: 100}, %{}, [:content, :price])
#Ecto.Changeset<action: nil, changes: %{}, errors: [],
data: #SampleProject.Post<>, valid?: true>
有一個 price 映射到20的轉換,但因為只允許 content 欄位進行轉換,所以資料不會改變。
iex> Ecto.Changeset.cast(%SampleProject.Post{content: "test", price: 100}, %{"price" => 20}, [:content])
#Ecto.Changeset<action: nil, changes: %{}, errors: [],
data: #SampleProject.Post<>, valid?: true>
price 欄位資料轉換為20。
iex> Ecto.Changeset.cast(%SampleProject.Post{content: "test", price: 100}, %{"price" => 20}, [:price])
#Ecto.Changeset<
action: nil,
changes: %{price: 20},
errors: [],
data: #SampleProject.Post<>,
valid?: true
cast/3
進行的任務就是資料的轉換,不會進行驗證,所以可以看到其output的 struct 中,valid? 都是 true。
change/2
與cast/3
功能很接近,但少了 cast 的過濾功能,當資料來源可信時,也是可以只用 change 即可。
驗證
Ecto.Changeset內提供了多驗證方法,可以查找這邊
iex> changeset = Post.changeset(%Post{}, %{title: "test title"})
#Ecto.Changeset<
action: nil,
changes: %{title: "test title"},
errors: [
content: {"can't be blank", [validation: :required]},
price: {"can't be blank", [validation: :required]}
],
data: #SampleProject.Post<>,
valid?: false
iex> changeset.valid?
false
iex> changeset.errors
[
content: {"can't be blank", [validation: :required]},
price: {"can't be blank", [validation: :required]}
]
若是能夠成功變更(新增)資料,應該會得到一個valid為true的結果,並且一個很方便的地方是,能夠從change直接查看到所發生改變的欄位值。
iex> changeset = Post.changeset(%Post{}, %{title: "test title", content: "test content", price: 18})
#Ecto.Changeset<
action: nil,
changes: %{content: "test content", price: 18, title: "test title"},
errors: [],
data: #SampleProject.Post<>,
valid?: true
新增數據
Repo.insert
這個函式可以對資料庫新增數據
iex> alias SampleProject.Repo
SampleProject.Repo
iex> Repo.insert(%Post{title: "test title"})
[debug] QUERY OK db=16.0ms decode=16.0ms idle=125.0ms
INSERT INTO "posts" ("title","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["test title", ~N[2020-10-08 16:13:53], ~N[2020-10-08 16:13:53]]
{:ok,
%SampleProject.Post{
__meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
content: nil,
id: 1,
inserted_at: ~N[2020-10-08 16:13:53],
price: nil,
title: "test title",
updated_at: ~N[2020-10-08 16:13:53]
}}
透過變更集,我們可以在新增資料時進行驗證抑或資料轉換:
iex> changeset = Post.changeset(%Post{}, %{title: "test title", content: "content"})
#Ecto.Changeset<
action: nil,
changes: %{content: "content", title: "test title"},
errors: [price: {"can't be blank", [validation: :required]}],
data: #SampleProject.Post<>,
valid?: false
>
iex> {:error, changeset} = Repo.insert(changeset)
{:error,
#Ecto.Changeset<
action: :insert,
changes: %{content: "content", title: "test title"},
errors: [price: {"can't be blank", [validation: :required]}],
data: #SampleProject.Post<>,
valid?: false
>}
iex> changeset = Post.changeset(%Post{}, %{title: "test title", content: "content", price: 100})
#Ecto.Changeset<
action: nil,
changes: %{content: "content", price: 100, title: "test title"},
errors: [],
data: #SampleProject.Post<>,
valid?: true
>
iex> {:error, changeset} = Repo.insert(changeset)
[debug] QUERY OK db=16.0ms idle=109.0ms
INSERT INTO "posts" ("content","price","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) RETURNING "id" ["content", 100, "test title", ~N[2020-10-08 16:33:36], ~N[2020-10-08 16:33:36]]
** (MatchError) no match of right hand side value: {:ok, %SampleProject.Post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, content: "content", id: 2, inserted_at: ~N[2020-10-08 16:33:36], price: 100, title: "test title", updated_at: ~N[2020-10-08 16:33:36]}}
取得資料
或是Repo.all
函式,可以抓出所有數據。
iex> Repo.all(Post)
[debug] QUERY OK source="posts" db=0.0ms idle=141.0ms
SELECT p0."id", p0."content", p0."price", p0."title", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 []
[
%SampleProject.Post{
__meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
content: nil,
id: 1,
inserted_at: ~N[2020-10-08 16:13:53],
price: nil,
title: "test title",
updated_at: ~N[2020-10-08 16:13:53]
}
]
或是Repo.one可以僅抓出一個數據
iex> Repo.one(Post)
[debug] QUERY OK source="posts" db=15.0ms idle=422.0ms
SELECT p0."id", p0."content", p0."price", p0."title", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 []
%SampleProject.Post{
__meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
content: nil,
id: 1,
inserted_at: ~N[2020-10-08 16:13:53],
price: nil,
title: "test title",
updated_at: ~N[2020-10-08 16:13:53]
}
all
與one
的差別在於是否用一個list回傳結構資料。
條件查詢
藉由使用Ecto.Query.from,我們可以建立條件查詢,並且自己定義回傳的資料結構於select對應的值。
iex> import Ecto.Query
Ecto.Query
iex> Repo.all(from u in Post, select: u.title)
[debug] QUERY OK source="posts" db=0.0ms idle=656.0ms
SELECT p0."title" FROM "posts" AS p0 []
["test title"]
iex> Repo.all(from u in Post, select: [title: u.title, content: u.content])
[debug] QUERY OK source="posts" db=0.0ms idle=781.0ms
SELECT p0."title", p0."content" FROM "posts" AS p0 []
[[title: "test title", content: nil]]
總結,Ecto的詳細用法,可以參閱官方文件
體驗過後,個人覺得 Ecto 設計算是十分精妙,也用了更漂亮的語法來完成資料查詢。
可以感覺到 Elixir 雖然在社群大小上還不算非常龐大,但在工具的成熟度上,感覺也完全不會輸其他語言的工具。